"""
Tkinter 图形用户界面模块。
"""
import tkinter as tk
from tkinter import ttk, filedialog, messagebox, scrolledtext
import threading
import logging
import webbrowser
from pathlib import Path
import os

from .downloader import TileDownloader
from .utils import lonlat_to_tile, get_global_tile_range, get_resource_path


class TileDownloaderGUI:
    """
    瓦片下载器的图形用户界面。
    """

    def __init__(self, root: tk.Tk):
        """
        初始化 GUI 窗口和组件。

        Args:
            root (tk.Tk): Tkinter 根窗口。
        """
        self.root = root
        try:
            from . import version
            VERSION = getattr(version, 'VERSION', '')
        except ImportError:
            VERSION = ''
        self.root.title("地图瓦片下载器 " + VERSION)
        # 设置窗口默认置顶
        self.root.wm_attributes("-topmost", False)
        self.is_topmost = False  # 跟踪置顶状态
        # 设置窗口图标
        icon_name = "logo/logo.ico"
        icon_path = get_resource_path(icon_name)
        self.root.iconbitmap(icon_path)
        self.root.geometry("700x680")  # 调整窗口大小
        self.download_thread: threading.Thread = None
        self.last_downloader: TileDownloader = None
        self.is_source_local = tk.BooleanVar(value=False)  # 跟踪源类型
        self.download_cancelled = False
        self._create_widgets()

    def _create_widgets(self):
        """创建所有 GUI 组件。"""
        # --- URL/路径 输入 ---
        url_frame = ttk.LabelFrame(self.root, text="瓦片源 (URL 或本地路径)")
        url_frame.pack(fill="x", padx=10, pady=5)
        self.url_var = tk.StringVar(value="")
        ttk.Label(url_frame, text="模板:").grid(row=0, column=0, sticky=tk.W, padx=5, pady=5)
        self.url_entry = ttk.Entry(url_frame, textvariable=self.url_var, width=65)
        self.url_entry.grid(row=0, column=1, padx=5, pady=5, sticky=(tk.W, tk.E))

        # 检测源按钮
        self.detect_button = ttk.Button(url_frame, text="检测源", command=self._detect_source)
        self.detect_button.grid(row=0, column=2, padx=5, pady=5)

        url_hint_entry = ttk.Entry(
            url_frame,
            foreground="gray"
        )
        url_hint_entry.insert(0, "例如: https://example.com/{z}/{y}/{x}.png 或 C:/tiles/{z}/{y}/{x}.png")
        url_hint_entry.config(state="readonly")
        url_hint_entry.grid(row=1, column=0, columnspan=2, sticky="ew", padx=5, pady=(0, 2))

        # 源类型指示标签
        self.source_type_label = ttk.Label(url_frame, text="源类型: 未知", foreground='gray')
        self.source_type_label.grid(row=1, column=1, columnspan=2, sticky=tk.E, padx=5, pady=(0, 2))

        url_frame.columnconfigure(1, weight=1)

        # --- 保存路径 ---
        output_frame = ttk.LabelFrame(self.root, text="保存路径")
        output_frame.pack(fill="x", padx=10, pady=5)
        # --- 修改默认值 ---
        # 使用 Path.cwd() 获取当前工作目录，然后拼接 'data' 子目录
        # 使用 .resolve() 获取绝对路径，.as_posix() 确保在所有系统上使用正斜杠（对 GUI 来说通常不是必须的，但更规范）
        default_output_path = (Path.cwd() / "data").resolve()
        self.output_var = tk.StringVar(value=str(default_output_path))

        ttk.Label(output_frame, text="目录:").grid(row=0, column=0, sticky=tk.W, padx=5, pady=5)
        self.output_entry = ttk.Entry(output_frame, textvariable=self.output_var, width=60)
        self.output_entry.grid(row=0, column=1, padx=5, pady=5, sticky=(tk.W, tk.E))
        self.output_button = ttk.Button(output_frame, text="浏览...", command=self._browse_output)
        self.output_button.grid(row=0, column=2, padx=5, pady=5)
        output_frame.columnconfigure(1, weight=1)

        # --- 下载级别 和 全球下载级别 (修改后放在同一行) ---
        combined_zoom_frame = ttk.LabelFrame(self.root, text="下载级别与全球设置")
        combined_zoom_frame.pack(fill="x", padx=10, pady=5)

        # --- 下载级别 (最小/最大) ---
        # ttk.Label(combined_zoom_frame, text="下载级别:").grid(row=0, column=0, sticky=tk.W, padx=5, pady=5) # 可选标签

        ttk.Label(combined_zoom_frame, text="最小:").grid(row=0, column=0, sticky=tk.W, padx=(5, 0), pady=5)  # 微调 padx
        self.min_zoom_var = tk.IntVar(value=1)
        self.min_zoom_spin = ttk.Spinbox(combined_zoom_frame, from_=0, to=22, textvariable=self.min_zoom_var, width=5)
        self.min_zoom_spin.grid(row=0, column=1, sticky=tk.W, padx=5, pady=5)
        self.min_zoom_var.trace_add("write", self._on_min_zoom_change)

        ttk.Label(combined_zoom_frame, text="最大:").grid(row=0, column=2, sticky=tk.W, padx=(10, 0),
                                                          pady=5)  # 增加左侧 padx 以区分组
        self.max_zoom_var = tk.IntVar(value=3)
        self.max_zoom_spin = ttk.Spinbox(combined_zoom_frame, from_=0, to=22, textvariable=self.max_zoom_var, width=5)
        self.max_zoom_spin.grid(row=0, column=3, sticky=tk.W, padx=5, pady=5)
        self.max_zoom_var.trace_add("write", self._on_max_zoom_change)

        # --- 全球下载级别 ---
        self.global_var = tk.BooleanVar()
        self.global_check = ttk.Checkbutton(
            combined_zoom_frame,
            text="全球下载级别:",
            variable=self.global_var
        )
        self.global_check.grid(row=0, column=4, sticky=tk.W, padx=(20, 5), pady=5)  # 使用 padx 分隔两组设置

        self.global_level_var = tk.IntVar(value=1)
        self.global_level_min = self.min_zoom_var.get()
        self.global_level_max = self.max_zoom_var.get()
        # 全球级别 Spinbox 的最大值绑定到最大下载级别
        self.global_level_spin = ttk.Spinbox(
            combined_zoom_frame,
            from_=self.global_level_min,
            to=self.global_level_max,
            textvariable=self.global_level_var,
            width=5
        )
        self.global_level_spin.grid(row=0, column=5, sticky=tk.W, padx=5, pady=5)

        # --- 下载范围 (经纬度) ---
        range_frame = ttk.LabelFrame(self.root, text="下载范围 (经纬度)")
        range_frame.pack(fill="x", padx=10, pady=5)

        # --- 裁切复选框 ---
        self.clip_var = tk.BooleanVar()  # 默认不勾选
        self.clip_check = ttk.Checkbutton(
            range_frame,
            text="裁切超出范围的瓦片",
            variable=self.clip_var
        )
        self.clip_check.grid(row=0, column=0, columnspan=3, sticky=tk.W, padx=5, pady=(0, 5))

        ttk.Label(range_frame, text="左上 (Lon, Lat):").grid(row=1, column=0, sticky=tk.W, padx=5, pady=5)
        self.lon_min_var = tk.DoubleVar(value=73.0)
        self.lon_min_entry = ttk.Entry(range_frame, textvariable=self.lon_min_var, width=10)
        self.lon_min_entry.grid(row=1, column=1, padx=5, pady=5)

        self.lat_max_var = tk.DoubleVar(value=53.0)
        self.lat_max_entry = ttk.Entry(range_frame, textvariable=self.lat_max_var, width=10)
        self.lat_max_entry.grid(row=1, column=2, padx=5, pady=5)

        ttk.Label(range_frame, text="右下 (Lon, Lat):").grid(row=2, column=0, sticky=tk.W, padx=5, pady=5)
        self.lon_max_var = tk.DoubleVar(value=136.0)
        self.lon_max_entry = ttk.Entry(range_frame, textvariable=self.lon_max_var, width=10)
        self.lon_max_entry.grid(row=2, column=1, padx=5, pady=5)

        self.lat_min_var = tk.DoubleVar(value=18.0)
        self.lat_min_entry = ttk.Entry(range_frame, textvariable=self.lat_min_var, width=10)
        self.lat_min_entry.grid(row=2, column=2, padx=5, pady=5)

        # --- 文件格式和路径格式 ---
        format_frame = ttk.LabelFrame(self.root, text="存储选项")
        format_frame.pack(fill="x", padx=10, pady=5)
        ttk.Label(format_frame, text="图片格式:").grid(row=0, column=0, sticky=tk.W, padx=5, pady=5)
        self.format_var = tk.StringVar(value="png")
        self.format_combo = ttk.Combobox(format_frame, textvariable=self.format_var, values=["png", "jpg"],
                                         state="readonly", width=10)
        self.format_combo.grid(row=0, column=1, sticky=tk.W, padx=5, pady=5)

        ttk.Label(format_frame, text="路径格式:").grid(row=0, column=2, sticky=tk.W, padx=(20, 5), pady=5)
        self.path_var = tk.StringVar(value="zyx")  # 修改默认值为 zxy
        # 修改选项为 zxy 和 zyx
        self.path_combo = ttk.Combobox(format_frame, textvariable=self.path_var, values=["zxy", "zyx"],
                                       state="readonly", width=10)
        self.path_combo.grid(row=0, column=3, sticky=tk.W, padx=5, pady=5)

        # --- 性能设置 ---
        perf_frame = ttk.LabelFrame(self.root, text="性能设置")
        perf_frame.pack(fill="x", padx=10, pady=5)
        ttk.Label(perf_frame, text="线程数:").grid(row=0, column=0, sticky=tk.W, padx=5, pady=5)
        self.max_workers_var = tk.IntVar(value=5)  # 默认值
        self.max_workers_spin = ttk.Spinbox(
            perf_frame,
            from_=1,  # 最小值
            to=8,  # 最大值
            textvariable=self.max_workers_var,
            width=5
        )
        self.max_workers_spin.grid(row=0, column=1, sticky=tk.W, padx=5, pady=5)
        ttk.Label(perf_frame, text="(1-8)", foreground='gray').grid(row=0, column=2, sticky=tk.W, padx=(5, 0), pady=5)

        # --- 控制按钮 ---
        button_frame = ttk.Frame(self.root)
        button_frame.pack(fill="x", padx=10, pady=10)
        self.start_button = ttk.Button(button_frame, text="开始下载", command=self._start_download)
        self.start_button.pack(side=tk.LEFT, padx=5)
        self.stop_button = ttk.Button(button_frame, text="停止", command=self._stop_download, state='disabled')
        self.stop_button.pack(side=tk.LEFT, padx=5)
        self.preview_button = ttk.Button(button_frame, text="预览", command=self._generate_preview)
        self.preview_button.pack(side=tk.LEFT, padx=5)
        self.topmost_button = ttk.Button(button_frame, text="置顶", command=self._toggle_topmost)
        self.topmost_button.pack(side=tk.RIGHT, padx=5)
        # --- 关于按钮 ---
        self.about_button = ttk.Button(button_frame, text="关于", command=self._show_about)
        self.about_button.pack(side=tk.RIGHT, padx=5)
        # --- 统计按钮 ---
        self.stats_button = ttk.Button(button_frame, text="统计", command=self._calculate_stats)
        self.stats_button.pack(side=tk.LEFT, padx=5)

        # --- 日志输出 ---
        log_frame = ttk.LabelFrame(self.root, text="日志")
        log_frame.pack(fill="both", expand=True, padx=10, pady=5)
        self.log_text = scrolledtext.ScrolledText(log_frame, state='disabled', height=12)
        self.log_text.pack(fill="both", expand=True, padx=5, pady=5)

        # 重定向 logging 到 Text 控件
        self.log_handler = TextHandler(self.log_text)
        logging.getLogger().addHandler(self.log_handler)
        logging.getLogger().setLevel(logging.INFO)  # 设置日志级别

    def _calculate_stats(self):
        """计算并显示指定范围和层级的瓦片数量统计，支持全球下载设置。"""
        try:
            # 1. 获取输入参数
            min_zoom = self.min_zoom_var.get()
            max_zoom = self.max_zoom_var.get()
            is_global_enabled = self.global_var.get()  # 获取全球下载是否启用

            if min_zoom > max_zoom:
                messagebox.showerror("统计错误", "最小级别不能大于最大级别。")
                return

            # 只有在需要计算区域瓦片时才获取范围坐标
            # (全球级别部分不需要，但区域部分从 min_zoom 开始需要)
            lon_min = self.lon_min_var.get()
            lat_min = self.lat_min_var.get()
            lon_max = self.lon_max_var.get()
            lat_max = self.lat_max_var.get()

            if not is_global_enabled:  # 如果不启用全球下载，则区域范围必须有效
                if lon_min >= lon_max or lat_min >= lat_max:
                    messagebox.showerror("统计错误", "范围坐标无效。")
                    return

            # 2. 计算瓦片数量
            stats_lines = []
            total_count = 0

            if is_global_enabled:
                # --- 启用了全球下载 ---
                global_level = self.global_level_var.get()

                if global_level < min_zoom or global_level > max_zoom:
                    messagebox.showerror("统计错误", "全球级别必须在最小级别和最大级别之间。")
                    return

                stats_lines.append(f"瓦片数量统计 (层级 {min_zoom} - {max_zoom}, 启用全球下载到级别 {global_level}):")
                stats_lines.append("-" * 50)

                # a. 计算全球级别部分 (min_zoom 到 global_level)
                stats_lines.append("全球瓦片部分:")
                for z in range(min_zoom, global_level + 1):
                    min_x, min_y, max_x, max_y = get_global_tile_range(z)
                    count = (max_x - min_x + 1) * (max_y - min_y + 1)
                    total_count += count
                    stats_lines.append(f"  全球 级别 {z:2d}: {count:>12,} 张")

                # b. 计算区域级别部分 (min_zoom 到 max_zoom)
                # 注意：这部分会覆盖全球部分在相同级别上的瓦片（这是设计意图）
                stats_lines.append("区域瓦片部分 (覆盖全球):")
                for z in range(min_zoom, max_zoom + 1):
                    # 计算该级别下的区域瓦片范围
                    x_min, y_max = lonlat_to_tile(lon_min, lat_min, z)  # lat_min -> y_max
                    x_max, y_min = lonlat_to_tile(lon_max, lat_max, z)  # lat_max -> y_min
                    tile_min_x, tile_max_x = min(x_min, x_max), max(x_min, x_max)
                    tile_min_y, tile_max_y = min(y_min, y_max), max(y_min, y_max)

                    count = (tile_max_x - tile_min_x + 1) * (tile_max_y - tile_min_y + 1)
                    # 注意：这里不再累加到 total_count，因为全球部分已经包含了所有级别
                    # 实际下载逻辑是先下全球，再下区域（会覆盖），所以总数量是：
                    # (全球 min_zoom->global_level 的总和) + (区域 (global_level+1)->max_level 的总和)
                    # 但为了显示清晰，我们分别列出两部分的数量，并计算一个“理论总和”（全球+区域）
                    # 和一个“实际下载总和”（按下载逻辑）

                    # 为简单起见，我们按“理论总和”显示（即全球部分 + 区域部分，有重叠）
                    # 如果需要精确的“实际下载数量”，逻辑会更复杂（需要从区域数中减去全球已覆盖的区域数）
                    # 这里采用更直观的显示方式
                    # total_count += count # 不再直接累加
                    stats_lines.append(f"  区域 级别 {z:2d}: {count:>12,} 张")

                # 计算“实际”下载的总瓦片数（更符合下载逻辑）
                # 全球部分: min_zoom -> global_level
                actual_total = 0
                for z in range(min_zoom, global_level + 1):
                    min_x, min_y, max_x, max_y = get_global_tile_range(z)
                    actual_total += (max_x - min_x + 1) * (max_y - min_y + 1)
                # 区域部分: (global_level + 1) -> max_zoom
                for z in range(global_level + 1, max_zoom + 1):
                    x_min, y_max = lonlat_to_tile(lon_min, lat_min, z)
                    x_max, y_min = lonlat_to_tile(lon_max, lat_max, z)
                    tile_min_x, tile_max_x = min(x_min, x_max), max(x_min, x_max)
                    tile_min_y, tile_max_y = min(y_min, y_max), max(y_min, y_max)
                    actual_total += (tile_max_x - tile_min_x + 1) * (tile_max_y - tile_min_y + 1)

                total_count = actual_total

            else:
                # --- 未启用全球下载 ---
                stats_lines.append(f"瓦片数量统计 (层级 {min_zoom} - {max_zoom}):")
                stats_lines.append("-" * 30)

                for z in range(min_zoom, max_zoom + 1):
                    x_min, y_max = lonlat_to_tile(lon_min, lat_min, z)
                    x_max, y_min = lonlat_to_tile(lon_max, lat_max, z)
                    tile_min_x, tile_max_x = min(x_min, x_max), max(x_min, x_max)
                    tile_min_y, tile_max_y = min(y_min, y_max), max(y_min, y_max)

                    count = (tile_max_x - tile_min_x + 1) * (tile_max_y - tile_min_y + 1)
                    total_count += count
                    stats_lines.append(f"  级别 {z:2d}: {count:>10,} 张")

            stats_lines.append("-" * 30)
            stats_lines.append(f"  总计:   {total_count:>10,} 张")

            # 3. 显示结果
            stats_text = "\n".join(stats_lines)
            messagebox.showinfo("瓦片统计", stats_text)

        except tk.TclError:
            messagebox.showerror("统计错误", "请输入有效的数字。")
        except Exception as e:
            messagebox.showerror("统计错误", f"计算过程中发生错误:\n{e}")

    def _show_about(self):
        """显示关于信息的对话框。"""
        # 尝试导入版本信息，如果失败则使用空字符串
        try:
            from . import version
            version_info = {
                'VERSION': getattr(version, 'VERSION', ''),
                'AUTHOR': getattr(version, 'AUTHOR', ''),
                'DATETIME': getattr(version, 'DATETIME', '')
            }
        except ImportError:
            version_info = {
                'VERSION': '',
                'AUTHOR': '',
                'DATETIME': ''
            }

        # 构建信息文本
        info_lines = [
            "地图瓦片下载器",
            f"版本: {version_info['VERSION']}",
            f"作者: {version_info['AUTHOR']}",
            f"构建时间: {version_info['DATETIME']}",
            "",  # 空行
            "一个使用 Python 和 Tkinter 构建的工具，",
            "用于下载在线或本地地图瓦片。"
        ]

        info_text = "\n".join(info_lines)

        # 使用 messagebox 显示信息
        messagebox.showinfo("关于", info_text)

    def _toggle_topmost(self):
        """切换窗口的置顶状态。"""
        self.is_topmost = not self.is_topmost
        self.root.wm_attributes("-topmost", self.is_topmost)

        # 更新按钮文本
        if self.is_topmost:
            self.topmost_button.config(text="取消置顶")
            # 可选：添加日志提示
            # logging.info("窗口已置顶")
        else:
            self.topmost_button.config(text="置顶")
            # 可选：添加日志提示
            # logging.info("窗口取消置顶")

    def _browse_output(self):
        """打开文件夹选择对话框。"""
        directory = filedialog.askdirectory()
        if directory:
            self.output_var.set(directory)

    def _on_max_zoom_change(self, *args):
        """当最大下载级别改变时，更新全球级别输入框的最大值。"""
        try:
            new_max = self.max_zoom_var.get()
            if new_max < self.min_zoom_var.get():
                new_max = self.min_zoom_var.get()
            self.global_level_max = new_max
            # 更新 Spinbox 的最大值
            self.global_level_spin.configure(to=self.global_level_max)
            # 确保全球级别不超过新的最大值
            if self.global_level_var.get() > self.global_level_max:
                self.global_level_var.set(self.global_level_max)
        except tk.TclError:
            pass

    def _on_min_zoom_change(self, *args):
        """当最小下载级别改变时，更新全球级别输入框的最小值。"""
        try:
            new_min = self.min_zoom_var.get()
            if new_min > self.min_zoom_var.get():
                new_min = self.min_zoom_var.get()
            self.global_level_min = new_min
            # 更新 Spinbox 的最小值
            self.global_level_spin.configure(from_=self.global_level_min)
            # 确保全球级别不超过新的最小值
            if self.global_level_var.get() < self.global_level_min:
                self.global_level_var.set(self.global_level_min)
        except tk.TclError:
            pass

    def _detect_source(self):
        """检测瓦片源是本地路径还是网络URL。"""
        source_template = self.url_var.get().strip()
        if not source_template:
            self.is_source_local.set(False)
            self.source_type_label.config(text="源类型: 未知", foreground='gray')
            messagebox.showwarning("检测源", "请先输入瓦片源模板。")
            return

        # 简单判断：如果模板以 http:// 或 https:// 开头，则认为是网络源
        if source_template.lower().startswith(("http://", "https://")):
            self.is_source_local.set(False)
            self.source_type_label.config(text="源类型: 网络URL", foreground='blue')
            logging.info("检测到源类型: 网络URL")
        else:
            # 否则认为是本地路径
            self.is_source_local.set(True)
            self.source_type_label.config(text="源类型: 本地路径", foreground='green')
            logging.info("检测到源类型: 本地路径")
        # 使能开始下载按钮（如果之前因未知源类型而禁用了）
        # self.start_button.config(state='normal') # 通常不需要，因为验证在开始时进行

    def _validate_inputs(self) -> bool:
        """验证用户输入。"""
        if not self.url_var.get().strip():
            messagebox.showerror("错误", "请输入瓦片源 URL 或路径模板。")
            return False
        if not self.output_var.get().strip():
            messagebox.showerror("错误", "请选择保存路径。")
            return False

        output_path = Path(self.output_var.get())

        # --- 检查并处理输出目录 ---
        if output_path.exists():
            if not output_path.is_dir():
                messagebox.showerror("错误", "保存路径必须是一个目录。")
                return False

            # 检查目录是否为空
            try:
                if any(output_path.iterdir()):  # 如果目录里有任何内容
                    # 弹出确认对话框
                    proceed = messagebox.askyesno(
                        "目录非空",
                        f"保存目录 '{output_path}' 不为空。\n"
                        "继续下载可能会覆盖同名文件。\n\n"
                        "您确定要继续吗？",
                        icon='warning'  # 显示警告图标
                    )
                    if not proceed:
                        logging.info("用户取消了下载，因为输出目录非空。")
                        return False  # 用户选择不继续
                # 如果目录为空或用户确认继续，则通过验证
            except PermissionError:
                messagebox.showerror("错误", f"没有权限访问保存目录: {output_path}")
                return False
        else:
            # 目录不存在，尝试创建
            try:
                output_path.mkdir(parents=True, exist_ok=True)
                logging.info(f"已创建保存目录: {output_path}")
            except Exception as e:
                messagebox.showerror("错误", f"无法创建保存目录 '{output_path}': {e}")
                return False

        try:
            min_z, max_z = self.min_zoom_var.get(), self.max_zoom_var.get()
            if min_z > max_z:
                messagebox.showerror("错误", "最小级别不能大于最大级别。")
                return False

            lon_min, lat_min = self.lon_min_var.get(), self.lat_min_var.get()
            lon_max, lat_max = self.lon_max_var.get(), self.lat_max_var.get()
            if lon_min >= lon_max or lat_min >= lat_max:
                messagebox.showerror("错误", "范围坐标无效。")
                return False

            if self.global_var.get():
                global_level = self.global_level_var.get()
                if global_level < min_z or global_level > max_z:
                    messagebox.showerror("错误", "全球级别必须在最小级别和最大级别之间。")
                    return False

            max_workers = self.max_workers_var.get()
            if not (1 <= max_workers <= 8):
                messagebox.showerror("错误", "线程数必须在 1 到 8 之间。")
                return False

        except tk.TclError:
            messagebox.showerror("错误", "请输入有效的数字。")
            return False

        return True  # 所有验证通过

    def _start_download(self):
        """启动下载线程。"""
        # 在开始前强制检测一次源类型，确保使用最新信息
        self._detect_source()

        if not self._validate_inputs():
            return  # 如果验证失败或用户取消，则直接返回

        if self.download_thread and self.download_thread.is_alive():
            messagebox.showwarning("警告", "下载已在进行中。")
            return
        self.download_cancelled = False
        self.start_button.config(state='disabled')
        self.stop_button.config(state='normal')
        # 检测源按钮在下载期间可以保持启用，允许用户再次检测
        # self.detect_button.config(state='disabled')
        logging.info("开始下载任务...")

        self.download_thread = threading.Thread(target=self._run_download, daemon=True)
        self.download_thread.start()

    def _run_download(self):
        """实际执行下载逻辑的函数，在后台线程运行。"""
        try:
            url_template = self.url_var.get().strip()
            output_dir = self.output_var.get().strip()
            tile_format = self.format_var.get()
            path_format = self.path_var.get()
            min_zoom = self.min_zoom_var.get()
            max_zoom = self.max_zoom_var.get()
            is_local_source = self.is_source_local.get()
            max_workers = self.max_workers_var.get()
            # --- 获取裁切选项 ---
            should_clip = self.clip_var.get()

            # --- 获取范围坐标 (用于裁切) ---
            lon_min = self.lon_min_var.get()
            lat_min = self.lat_min_var.get()
            lon_max = self.lon_max_var.get()
            lat_max = self.lat_max_var.get()
            clip_bounds = (lon_min, lat_min, lon_max, lat_max) if should_clip else None

            self.last_downloader = TileDownloader(
                url_template=url_template,
                output_dir=output_dir,
                zoom_levels=(min_zoom, max_zoom),
                tile_format=tile_format,
                path_format=path_format,
                is_local_source=is_local_source,
                max_workers=max_workers,
                clip_tiles=should_clip,
                clip_bounds=clip_bounds
            )

            global_level = -1
            if self.global_var.get():
                global_level = self.global_level_var.get()
                if self.download_cancelled:
                    logging.info("下载已被用户取消 (全球下载开始前)。")
                    return
                logging.info(f"开始下载全球瓦片，级别 {min_zoom} 到 {global_level}...")
                for z in range(min_zoom, global_level + 1):
                    if self.download_cancelled:
                        logging.info(f"下载已被用户取消 (全球级别 {z})。")
                        return
                    min_x, min_y, max_x, max_y = get_global_tile_range(z)
                    logging.info(f"Downloading global level {z} (Tile Range: {min_x},{min_y} to {max_x},{max_y})")
                    # 全球下载不裁切
                    self.last_downloader.download_range(min_x, min_y, max_x, max_y, z, clip_override=False,
                                                        cancelled_flag=lambda: self.download_cancelled)
                logging.info(f"全球瓦片下载完成 (级别 {min_zoom}-{global_level})。")
            if self.download_cancelled:
                logging.info("下载已被用户取消 (区域下载开始前)。")
                return
            if global_level != -1:
                min_zoom = global_level + 1
            logging.info(f"开始下载指定区域瓦片，级别 {min_zoom} 到 {max_zoom}...")
            for z in range(min_zoom, max_zoom + 1):
                if self.download_cancelled:
                    logging.info(f"下载已被用户取消 (区域级别 {z})。")
                    return
                x_min, y_max = lonlat_to_tile(lon_min, lat_min, z)
                x_max, y_min = lonlat_to_tile(lon_max, lat_max, z)
                min_x, max_x = min(x_min, x_max), max(x_min, x_max)
                min_y, max_y = min(y_min, y_max), max(y_min, y_max)
                logging.info(f"Downloading level {z} (Tile Range: {min_x},{min_y} to {max_x},{max_y})")
                # 区域下载根据实例设置决定是否裁切
                self.last_downloader.download_range(min_x, min_y, max_x, max_y, z,
                                                    clip_override=None,
                                                    cancelled_flag=lambda: self.download_cancelled)  # None 表示使用实例默认设置
            logging.info(f"指定区域瓦片下载完成 (级别 {min_zoom}-{max_zoom})。")

            logging.info("所有下载任务已完成。")

        except Exception as e:
            logging.error(f"下载过程中发生未预期错误: {e}")
        finally:
            self.root.after(0, self._on_download_finished)

    def _stop_download(self):
        """停止下载"""
        if self.download_thread and self.download_thread.is_alive():
            logging.info("正在请求停止下载...")
            # 设置取消标志
            self.download_cancelled = True
            # 禁用停止按钮，防止重复点击
            self.stop_button.config(state='disabled')
            # 通知用户请求已发送
            logging.info("停止请求已发送。正在等待当前任务完成... (这可能需要一些时间)")
            # 注意：实际的线程终止由下载逻辑内部的检查点实现
        else:
            logging.info("没有正在运行的下载任务。")
            self.stop_button.config(state='disabled')

    def _on_download_finished(self):
        """下载线程结束后调用，恢复 GUI 状态。"""
        # 无论是否取消，都恢复按钮状态
        self.start_button.config(state='normal')
        self.stop_button.config(state='disabled')
        self.preview_button.config(state='normal')
        # 重置取消标志，为下次下载做准备
        self.download_cancelled = False
        # self.download_thread = None # 可以保留引用或重置

    def _generate_preview(self):
        """生成并打开预览 HTML 文件。"""
        output_dir = Path(self.output_var.get())
        os.makedirs(output_dir, exist_ok=True)
        if not output_dir.exists() or not output_dir.is_dir():
            messagebox.showerror("错误", "保存路径无效或不存在。")
            return

        html_content = self._create_leaflet_html()
        html_file_path = output_dir / "preview.html"

        try:
            with open(html_file_path, 'w', encoding='utf-8') as f:
                f.write(html_content)
            logging.info(f"预览 HTML 已生成: {html_file_path}")
            # 在浏览器中打开
            webbrowser.open(html_file_path.as_uri())  # 使用 as_uri() 获取 file:// URL
            logging.info("已在浏览器中打开预览。")
        except Exception as e:
            logging.error(f"生成或打开预览文件失败: {e}")
            messagebox.showerror("错误", f"生成预览文件失败: {e}")

    def _create_leaflet_html(self) -> str:
        """创建用于预览的 Leaflet HTML 内容。"""
        # 获取下载器配置
        # path_format = self.last_downloader.path_format
        # tile_format = self.last_downloader.tile_format
        # min_zoom = self.last_downloader.min_zoom
        # max_zoom = self.last_downloader.max_zoom

        # 以下获取的是实时输入框内容
        tile_format = self.format_var.get()
        path_format = self.path_var.get()
        min_zoom = self.min_zoom_var.get()
        max_zoom = self.max_zoom_var.get()

        # --- 动态生成瓦片 URL 模板 ---
        # Leaflet 的 {z}, {x}, {y} 与我们的模板一致
        # 我们需要根据 path_format 构建相对于 output_dir 的路径
        if path_format == 'zyx':
            # 例如: output_dir/z/y/x.png
            # 相对于 output_dir 的 URL 是: {z}/{y}/{x}.png
            tile_url_template = f"{{z}}/{{y}}/{{x}}.{tile_format}"
        else:  # 默认 'zxy'
            # 例如: output_dir/z/x/y.png
            # 相对于 output_dir 的 URL 是: {z}/{x}/{y}.png
            tile_url_template = f"{{z}}/{{x}}/{{y}}.{tile_format}"

        # 使用 urllib.parse.quote 将路径安全地编码为URL
        # safe_output_dir_url = urllib.parse.quote(output_dir.as_posix(), safe='/:')  # 保留 / 和 :

        # HTML 模板
        html_template = f"""
<!DOCTYPE html>
<html>
<head>
    <title>瓦片下载预览</title>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
     integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
     crossorigin=""/>
    <style>
      * {{
        padding: 0;
        margin: 0;
      }}

      html,
      body,
      #map {{
        height: 100%;
        width: 100%;
      }}
    </style>
</head>
<body>
    <div id="map"></div>
    <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
     integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
     crossorigin=""></script>
    <script>
        // 初始化地图
        var map = L.map('map',{{
            center: [36, 120],
            maxZoom: 18,
            zoom: 3,
            attributionControl: false,
        }});

        // 添加一个基础图层（用于对比）
        var World_Imagery = L.tileLayer('https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{{z}}/{{y}}/{{x}}', {{
             maxZoom: 18,
             zIndex: 0,
        }});
        var World_Topo_Map = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{{z}}/{{y}}/{{x}}', {{
             maxZoom: 18,
             zIndex: 0,
        }});
        var World_Street_Map = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/{{z}}/{{y}}/{{x}}', {{
             maxZoom: 18,
             zIndex: 0,
        }});

        // 添加下载的瓦片图层
        let urlArr = document.location.href.split("/");
        urlArr.pop();
        var customTiles = L.tileLayer( urlArr.join("/") + '/{tile_url_template}', {{
            // minZoom: {min_zoom},
            maxZoom: {max_zoom},
            tileSize: 256,
            zIndex: 999,
        }}).addTo(map);
        var polygonCoords = createRectangleFace2D([{self.lon_min_var.get()},{self.lat_max_var.get()}],[{self.lon_max_var.get()},{self.lat_min_var.get()}]);
        var polygon = L.polygon(
            polygonCoords.map((e) => e.reverse()),
            {{
              color: "red",
              fillColor: "#3388ff",
              fillOpacity: 0.2,
            }}
        ).addTo(map);
        // ArcGIS底图仅做切图参考
        L.control
        .layers(
          {{}},
          {{ World_Imagery, World_Topo_Map, World_Street_Map, "效果图": customTiles, "所需区域": polygon }}
        ).addTo(map);
        map.flyTo(polygon.getCenter(), 3);
        // 添加缩放级别显示
        var zoomControl = L.control({{ position: 'topright' }});
        zoomControl.onAdd = function(map) {{
            var div = L.DomUtil.create('div', 'leaflet-control-layers leaflet-control');
            div.innerHTML = 'Zoom level: ' + map.getZoom();
            map.on('zoomend', function() {{
                div.innerHTML = 'Zoom level: ' + map.getZoom();
            }});
            return div;
        }};
        zoomControl.addTo(map);
        
        // 计算四个角的经纬度坐标 topLeft,bottomRight 
        function createRectangleFace2D([lon1, lat1], [lon2, lat2]) {{
          return [
            [lon1, lat1],
            [lon1, lat2],
            [lon2, lat2],
            [lon2, lat1],
            [lon2, lat1],
            [lon1, lat1]
          ];
        }}
    </script>
</body>
</html>
"""
        return html_template


# --- 自定义日志处理器 ---
import tkinter as tk


class TextHandler(logging.Handler):
    """将日志输出重定向到 Tkinter Text 控件的处理器。"""

    def __init__(self, text_widget: tk.Text):
        super().__init__()
        self.text_widget = text_widget

    def emit(self, record):
        msg = self.format(record)

        def append():
            self.text_widget.configure(state='normal')
            self.text_widget.insert(tk.END, msg + '\n')
            self.text_widget.configure(state='disabled')
            # 自动滚动到底部
            self.text_widget.yview(tk.END)

        # 在主线程中安全地更新 GUI
        self.text_widget.after(0, append)
